import java.util.ArrayList;
import java.util.List;
import events.*;

public class Board {
	private GameSettings settings;
	private int[][] jewels;
	
	public Board(final GameSettings gameSettings) {
		this.settings = gameSettings;
		this.fillBoard();
	}
	
	// if possible, swaps(x1, y1) and (x2, y2) and
	// calls the callback function which lists the board events
	public final List<Event> swap(final int x1, final int y1, final int x2, final int y2) {
		int tmp;
		List<Event> events = null;
		
		if (this.canSwap(x1, y1, x2, y2)) {
			// swap the jewels
			tmp = this.getJewel(x1, y1);
			jewels[x1][y1] = this.getJewel(x2, y2);
			jewels[x2][y2] = tmp;
			
			// check the board and get a list of events
			events = this.check(null);
		}
		
		return events;
	}
	
	private void fillBoard() {
		int type;
		
		this.jewels = new int[this.settings.getCols()][this.settings.getRows()];
		
		for (int x = 0; x < this.jewels.length; x++) {
			for (int y = 0; y < this.jewels[x].length; y++) {
				do {
					type = this.getRandomJewel();
				} while ((type == this.getJewel(x - 1, y) 
						  && type == this.getJewel(x - 2, y))
						  || (type == this.getJewel(x, y - 1)
						  && type == this.getJewel(x, y - 2)));
				
				jewels[x][y] = type;
			}
		}
		
		if (!this.hasMoves()) {
			this.fillBoard();
		}
	}
		
	private int getJewel(final int x, final int y) {
		if (x < 0 || x > this.settings.getCols() - 1 || y < 0 || y > this.settings.getRows() - 1) {
			return -1;
		} else {
			return this.jewels[x][y];
		}
	}
	
	private int getRandomJewel() {
		return (int) (Math.random() * this.settings.getNumJewelTypes());
	}
		
	// returns true if (x1, y1) can be swapped with (x2, y2)
	// to form a new match
	private boolean canSwap(final int x1, final int y1, final int x2, final int y2) {
		int type1 = getJewel(x1, y1);
		int type2 = getJewel(x2, y2);
		boolean chain;
		
		if (!this.isAdjacent(x1, y1, x2, y2)) {
			return false;
		}
		
		// temporarily swap jewels
		this.jewels[x1][y1] = type2;
		this.jewels[x2][y2] = type1;
		
		chain = this.checkChain(x2, y2) > 2 || this.checkChain(x1, y1) > 2;
		
		// swap back
		this.jewels[x1][y1] = type1;
		this.jewels[x2][y2] = type2;
		
		return chain;
	}
	
	private boolean isAdjacent(final int x1, final int y1, final int x2, final int y2) {
		int dx = Math.abs(x1 - x2);
		int dy = Math.abs(y1 - y2);
		
		return (dx + dy == 1);
	}
	
	// returns the number of jewels in the longest chain
	// that includes (x, y)
	private int checkChain(final int x, final int y) {
		int type = getJewel(x, y),
			left = 0, right = 0,
			down = 0, up = 0;
		
		// look right
		while (type == this.getJewel(x + right + 1, y)) {
			right++;
		}
		
		// look left
		while (type == this.getJewel(x - left - 1, y)) {
			left++;
		}
		
		while (type == this.getJewel(x, y + up + 1)) {
			up++;
		}
		
		while (type == this.getJewel(x, y - down - 1)) {
			down++;
		}
		
		return Math.max(left + 1 + right, up + 1 + down);
	}
	
	private List<Event> check(final List<Event> events) {
		int[][] chains = getChains();
		boolean hadChains = false;
		int score = 0;
		List<RemoveEvent> removed = new ArrayList<RemoveEvent>();
		List<MoveEvent> moved = new ArrayList<MoveEvent>();
		int[] gaps = new int[this.settings.getCols()];
		int x, y;
		List<Event> returnEvents = events;
		
		if (returnEvents == null) {
			returnEvents = new ArrayList<Event>();
		}
				
		for (x = 0; x < this.settings.getCols(); x++) {
			gaps[x] = 0;
			for (y = this.settings.getRows() - 1; y >= 0; y--) {
				if (chains[x][y] > 2) {
					hadChains = true;
					gaps[x]++;
					removed.add(new RemoveEvent(x, y, this.getJewel(x, y)));
					
					// add points to score
					score += this.settings.getBaseScore() * Math.pow(2, (chains[x][y] - this.settings.getChainLength()));
					
				} else if (gaps[x] > 0) {
					moved.add(new MoveEvent(x, y + gaps[x], x, y, this.getJewel(x, y)));
					
					this.jewels[x][y + gaps[x]] = this.getJewel(x, y);
				}
			}
			
			// fill in new jewels from top
			for (y = 0; y < gaps[x]; y++) {
				this.jewels[x][y] = this.getRandomJewel();
				moved.add(new MoveEvent(x, y, x, y - gaps[x], this.jewels[x][y]));
			}
		}
		
		if (hadChains) {
			returnEvents.addAll(removed);
			returnEvents.addAll(moved);
			returnEvents.add(new ScoreEvent(score));
			
			// refill if there are no moves
			if (!this.hasMoves()) {
				this.fillBoard();
				events.add(new RefillEvent(this.getBoard()));
			}
			
			return this.check(returnEvents);
		} else {
			return returnEvents;
		}
	}
	
	private int[][] getChains() {
		int x, y;
		int[][] chains = new int[this.settings.getCols()][this.settings.getRows()];
		
		for (x = 0; x < chains.length; x++) {
			for (y = 0; y < chains[x].length; y++) {
				chains[x][y] = this.checkChain(x, y);
			}
		}
		
		return chains;
	}
	
	// returns true if at least one match can be made
	private boolean hasMoves() {
		int x, y;
		
		for (x = 0; x < this.settings.getCols(); x++) {
			for (y = 0; y < this.settings.getRows(); y++) {
				if (this.canJewelMove(x, y)) {
					return true;
				}
			}
		}
		
		return false;
	}
	
	public int [][] getBoard() {
		int[][] copy = new int[this.settings.getCols()][this.settings.getRows()];
		
		for (int x = 0; x < copy.length; x++) {
			for (int y = 0; y < copy[x].length; y++) {
				copy[x][y] = this.jewels[x][y];
			}
		}
		
		return copy;
	}
		
	// returns true if (x, y) is a valid position and if the jewel
	// at that position can be swapped with a neighbor
	private boolean canJewelMove(final int x, final int y) {
		return ((x > 0 && this.canSwap(x, y, x - 1, y)) 
				|| (x < this.settings.getCols() - 1 && this.canSwap(x, y, x + 1, y)) 
				|| (y > 0 && this.canSwap(x, y, x, y - 1)) 
				|| (y < this.settings.getRows() - 1 && this.canSwap(x, y, x, y + 1)));
	}
	
	public final void print() {
		String str = " ";
		int x, y;
		
		for (y = 0; y < this.settings.getRows(); y++) {
			for (x = 0; x < this.settings.getCols(); x++) {
				str += this.getJewel(x, y) + " ";				
			}
			
			str += "\r\n";
		}
		
		System.out.println(str);
	}
}